//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#include "cscreen.h"
#include <deque>
#include <unistd.h>
#include <termios.h>
#include <term.h>
#include <curses.h>
#include <csignal>
#include <sys/ioctl.h>

//these are defined in curses and clash with our key names
#undef KEY_DOWN
#undef KEY_UP
#undef KEY_LEFT
#undef KEY_RIGHT
#undef KEY_ENTER
#undef KEY_END
#undef KEY_HOME
#undef KEY_BACKSPACE

namespace buola { namespace tty {
    
std::unique_ptr<CScreen> CScreen::mDefaultScreen;
USignal<void()> CScreen::mWINCHSignal;
bool CScreen::mWINCHMutex=false;
bool CScreen::mWINCHTriggered=false;

CScreen::CScreen()
{
    mIn=STDIN_FILENO;
    mOut=STDOUT_FILENO;
        
    tcgetattr(mIn,&mOriginal.mAttrs);
    mOriginal.mInputMode=MODE_COOKED;
    mOriginal.mKeypad=false;
    mOriginal.mEcho=true;
    mOriginal.mAutowrap=true;
    mOriginal.mCursor=true;
    mCurrent=mOriginal;
    
    std::signal(SIGWINCH,DispatchWINCH);
    InitTerminfo();
}
    
CScreen::~CScreen()
{
    RestoreState(mOriginal);
}

void CScreen::InitTerminfo()
{
    setupterm(nullptr,mOut,nullptr);
}

void CScreen::SetAttrs(SState &pS)
{
    tcsetattr(mIn,TCSADRAIN,&pS.mAttrs);
}

void CScreen::WriteSeq(const char* pSeq)
{
    if(pSeq)
        Output(pSeq);
}
    
void CScreen::AddKey(const char *pKey,EKeyCode pCode,EKeyMod pMod)
{
    if(!pKey||pKey[0]==0) return;
    mKeyTrie->Add(pKey,make_key_char(pCode,pMod));
}
    
void CScreen::InitKeyTrie()
{
    mKeyTrie.reset(new TKeyTrie);

    AddKey(key_backspace,KEY_BACKSPACE);
    AddKey(key_down,KEY_DOWN);
    AddKey(key_up,KEY_UP);
    AddKey(key_left,KEY_LEFT);
    AddKey(key_right,KEY_RIGHT);
    AddKey("\e[B",KEY_DOWN);
    AddKey("\e[A",KEY_UP);
    AddKey("\e[D",KEY_LEFT);
    AddKey("\e[C",KEY_RIGHT);
    AddKey("\e[F",KEY_END);
    AddKey("\e[H",KEY_HOME);
    AddKey("\177",KEY_BACKSPACE);
    AddKey(key_end,KEY_END);
    AddKey(key_home,KEY_HOME);
    AddKey(key_npage,KEY_PGDN);
    AddKey(key_ppage,KEY_PGUP);
    AddKey(key_enter,KEY_ENTER);
    AddKey(key_f1,KEY_F1);
    AddKey(key_f2,KEY_F2);
    AddKey(key_f3,KEY_F3);
    AddKey(key_f4,KEY_F4);
    AddKey(key_f5,KEY_F5);
    AddKey(key_f6,KEY_F6);
    AddKey(key_f7,KEY_F7);
    AddKey(key_f8,KEY_F8);
    AddKey(key_f9,KEY_F9);
    AddKey(key_f10,KEY_F10);
    AddKey(key_f11,KEY_F11);
    AddKey(key_f12,KEY_F12);
    AddKey(key_dc,KEY_DEL);
    AddKey(key_ic,KEY_INS);
    AddKey(key_sleft,KEY_LEFT,KEYMOD_SHIFT);
    AddKey(key_sright,KEY_RIGHT,KEYMOD_SHIFT);
    AddKey("\e[1;5D",KEY_LEFT,KEYMOD_CONTROL);
    AddKey("\e[1;5C",KEY_RIGHT,KEYMOD_CONTROL);
    AddKey("\e[1;5B",KEY_DOWN,KEYMOD_CONTROL);
    AddKey("\e[1;5A",KEY_UP,KEYMOD_CONTROL);
    AddKey("\e[1;6D",KEY_LEFT,KEYMOD_CONTROL|KEYMOD_SHIFT);
    AddKey("\e[1;6C",KEY_RIGHT,KEYMOD_CONTROL|KEYMOD_SHIFT);
    AddKey("\e[1;2F",KEY_END,KEYMOD_SHIFT);
    AddKey("\e[1;2H",KEY_HOME,KEYMOD_SHIFT);
    AddKey("\e[1;2B",KEY_DOWN,KEYMOD_SHIFT);
    AddKey("\e[1;2A",KEY_UP,KEYMOD_SHIFT);
    AddKey("\e1",KEY_1,KEYMOD_ALT);
    AddKey("\e2",KEY_2,KEYMOD_ALT);
    AddKey("\e3",KEY_3,KEYMOD_ALT);
    AddKey("\e4",KEY_4,KEYMOD_ALT);
    AddKey("\e5",KEY_5,KEYMOD_ALT);
    AddKey("\e6",KEY_6,KEYMOD_ALT);
    AddKey("\e7",KEY_7,KEYMOD_ALT);
    AddKey("\e8",KEY_8,KEYMOD_ALT);
    AddKey("\e9",KEY_9,KEYMOD_ALT);
    AddKey("\e0",KEY_0,KEYMOD_ALT);
    AddKey("\ea",KEY_A,KEYMOD_ALT);
    AddKey("\eb",KEY_B,KEYMOD_ALT);
    AddKey("\ec",KEY_C,KEYMOD_ALT);
    AddKey("\ed",KEY_D,KEYMOD_ALT);
    AddKey("\ee",KEY_E,KEYMOD_ALT);
    AddKey("\ef",KEY_F,KEYMOD_ALT);
    AddKey("\eg",KEY_G,KEYMOD_ALT);
    AddKey("\eh",KEY_H,KEYMOD_ALT);
    AddKey("\ei",KEY_I,KEYMOD_ALT);
    AddKey("\ej",KEY_J,KEYMOD_ALT);
    AddKey("\ek",KEY_K,KEYMOD_ALT);
    AddKey("\el",KEY_L,KEYMOD_ALT);
    AddKey("\em",KEY_M,KEYMOD_ALT);
    AddKey("\en",KEY_N,KEYMOD_ALT);
    AddKey("\eo",KEY_O,KEYMOD_ALT);
    AddKey("\ep",KEY_P,KEYMOD_ALT);
    AddKey("\eq",KEY_Q,KEYMOD_ALT);
    AddKey("\er",KEY_R,KEYMOD_ALT);
    AddKey("\es",KEY_S,KEYMOD_ALT);
    AddKey("\et",KEY_T,KEYMOD_ALT);
    AddKey("\eu",KEY_U,KEYMOD_ALT);
    AddKey("\ev",KEY_V,KEYMOD_ALT);
    AddKey("\ew",KEY_W,KEYMOD_ALT);
    AddKey("\ex",KEY_X,KEYMOD_ALT);
    AddKey("\ey",KEY_Y,KEYMOD_ALT);
    AddKey("\ez",KEY_Z,KEYMOD_ALT);
    AddKey("\001",KEY_A,KEYMOD_CONTROL);
    AddKey("\002",KEY_B,KEYMOD_CONTROL);
    AddKey("\003",KEY_C,KEYMOD_CONTROL);
    AddKey("\004",KEY_D,KEYMOD_CONTROL);
    AddKey("\005",KEY_E,KEYMOD_CONTROL);
    AddKey("\006",KEY_F,KEYMOD_CONTROL);
    AddKey("\007",KEY_G,KEYMOD_CONTROL);
    AddKey("\012",KEY_J,KEYMOD_CONTROL);
    AddKey("\013",KEY_K,KEYMOD_CONTROL);
    AddKey("\014",KEY_L,KEYMOD_CONTROL);
    AddKey("\016",KEY_N,KEYMOD_CONTROL);
    AddKey("\017",KEY_O,KEYMOD_CONTROL);
    AddKey("\020",KEY_P,KEYMOD_CONTROL);
    AddKey("\021",KEY_Q,KEYMOD_CONTROL);
    AddKey("\022",KEY_R,KEYMOD_CONTROL);
    AddKey("\023",KEY_S,KEYMOD_CONTROL);
    AddKey("\024",KEY_T,KEYMOD_CONTROL);
    AddKey("\025",KEY_U,KEYMOD_CONTROL);
    AddKey("\026",KEY_V,KEYMOD_CONTROL);
    AddKey("\027",KEY_W,KEYMOD_CONTROL);
    AddKey("\030",KEY_X,KEYMOD_CONTROL);
    AddKey("\031",KEY_Y,KEYMOD_CONTROL);
    AddKey("\032",KEY_Z,KEYMOD_CONTROL);
    //we want these as keys:
    AddKey("\r",KEY_ENTER);
    AddKey("\t",KEY_TAB);
    AddKey("\010",KEY_BACKSPACE);
    AddKey("\e[Z",KEY_TAB,KEYMOD_SHIFT);
}
    
void CScreen::FillFIFO()
{
    unsigned char lChar=0;
    int lN;
    
    errno=0;
    
    do
    {
        lN=read(STDIN_FILENO,&lChar,1);
    }
    while(lN<=0&&errno==EINTR);

    if(lN<=0)
    {
        throw XIO("couldn't read from terminal in tty::fill_fifo");
    }
        
    mFIFO.push_back(lChar);
}

char CScreen::EmptyFIFO()
{
    int lRet=mFIFO.front();
    mFIFO.pop_front();
    return lRet;
}

void CScreen::ClearFIFO()
{
    mFIFO.clear();
}

void CScreen::DispatchWINCH(int)
{
    if(mWINCHMutex)
    {
        mWINCHTriggered=true;
        return;
    }
    mWINCHSignal();
}

void CScreen::RestoreState(CScreen::SState& pS)
{
    //this restores input mode and echo
    SetAttrs(pS);
    SetKeypad(pS.mKeypad);
    SetAutowrap(pS.mAutowrap);
    SetCursor(pS.mCursor);

    mCurrent=pS;
}

void CScreen::PushState()
{
    mStateStack.push(mCurrent);
}

void CScreen::PopState()
{
    RestoreState(mStateStack.top());
    mStateStack.pop();
}

void CScreen::SetInputMode(EInputMode pMode)
{
    if(pMode==mCurrent.mInputMode) return;
    
    if(pMode==MODE_RAW)
    {
        mCurrent.mAttrs.c_lflag&=~(ICANON|ISIG|IEXTEN);
        mCurrent.mAttrs.c_iflag&=~(IXON|BRKINT|PARMRK|ICRNL|INLCR);
        mCurrent.mAttrs.c_cc[VMIN]=1;
        mCurrent.mAttrs.c_cc[VTIME]=0;
    }
    else if(pMode==MODE_CBREAK)
    {
        mCurrent.mAttrs.c_lflag&=~ICANON;
        mCurrent.mAttrs.c_lflag|=ISIG;
        mCurrent.mAttrs.c_iflag&=~ICRNL;
        mCurrent.mAttrs.c_cc[VMIN]=1;
        mCurrent.mAttrs.c_cc[VTIME]=0;
    }
    else //COOKED
    {
        mCurrent.mAttrs.c_lflag|=ISIG|ICANON|(mOriginal.mAttrs.c_lflag&IEXTEN);        
        mCurrent.mAttrs.c_iflag|=mOriginal.mAttrs.c_lflag&(IXON|BRKINT|PARMRK|ICRNL);        
    }

    SetAttrs(mCurrent);
    
    mCurrent.mInputMode=pMode;
}
    
void CScreen::SetKeypad(bool pOn)
{
    if(pOn==mCurrent.mKeypad) return;
    
    if(pOn)
    {
        WriteSeq(keypad_xmit);
    }
    else
    {
        WriteSeq(keypad_local);
    }
    
    mCurrent.mKeypad=pOn;
}

void CScreen::SetEcho(bool pOn)
{
    if(pOn==mCurrent.mEcho) return;
    
    if(pOn)
    {
        mCurrent.mAttrs.c_lflag|=ECHO|ECHONL;
    }
    else
    {
        mCurrent.mAttrs.c_lflag&=~(ECHO|ECHONL);
    }
    
    SetAttrs(mCurrent);

    mCurrent.mEcho=pOn;    
}

void CScreen::SetAutowrap(bool pOn)
{
    if(pOn==mCurrent.mAutowrap) return;
    
    if(pOn)
    {
        WriteSeq(enter_am_mode);
    }
    else
    {
        WriteSeq(exit_am_mode);
    }
    
    mCurrent.mAutowrap=pOn;
}

void CScreen::SetCursor(bool pOn)
{
    if(pOn==mCurrent.mCursor) return;
    
    if(pOn)
    {
        WriteSeq(cursor_normal);
    }
    else
    {
        WriteSeq(cursor_invisible);
    }
    
    mCurrent.mCursor=pOn;
}

std::pair< int, int > CScreen::GetWinSize()
{
    struct winsize lSize;
    
    ioctl(mOut,TIOCGWINSZ,&lSize);
    
    return std::make_pair(lSize.ws_row,lSize.ws_col);
}

int CScreen::ReadRawChar()
{
    if(mFIFO.empty())
        FillFIFO();
    return EmptyFIFO();
}

bool CScreen::CanGetChar()
{
    if(!mFIFO.empty()) return true;

    struct timespec lTimeSpec={0,0};
    fd_set lDesc;
    FD_ZERO(&lDesc);
    FD_SET(STDIN_FILENO,&lDesc);
    pselect(STDIN_FILENO+1,&lDesc,nullptr,nullptr,&lTimeSpec,nullptr);

    return FD_ISSET(STDIN_FILENO,&lDesc);
}

int CScreen::GetChar()
{
    if(!mKeyTrie) InitKeyTrie();

    if(mFIFO.empty())
        FillFIFO();

    if(!GetKeypad())
        return EmptyFIFO();
    
    while(true)
    {
        //check for a key
        auto lB=mFIFO.begin(),lE=mFIFO.end();
        const int *lKey=mKeyTrie->Find(lB,lE);
        
        if(lB!=lE)
        {
            if(mFIFO.size()==1)
                return EmptyFIFO();
            else
            {
                msg_warn() << "unknown sequence... clearing queue\n";
                ClearFIFO();
            }
        }
        else if(lKey)
        {
            ClearFIFO();
            return *lKey;
        }
        FillFIFO();
    }
}

wchar_t CScreen::GetWChar()
{
    if(!mKeyTrie) InitKeyTrie();

    if(mFIFO.empty())
        FillFIFO();

    if(!GetKeypad())
        return EmptyFIFO();
    
    while(true)
    {
        //check for a key
        auto lB=mFIFO.begin(),lE=mFIFO.end();
        const int *lKey=mKeyTrie->Find(lB,lE);
        
        if(lB!=lE)
        {
            //it is not a key
            if(lB!=mFIFO.begin())
            {
                //it looked like a key, so it is probably garbage
                msg_debug<20>() << "unknown key sequence";
                for(auto i=mFIFO.begin();i!=mFIFO.end();++i)
                    msg_debug<20>() << " " << (int)*i;
                msg_debug<20>() << "\n";
                ClearFIFO();
            }
            else
            {
                mbstate_t lState;
                memset(&lState,0,sizeof(lState));
        
                std::string lString(mFIFO.begin(),mFIFO.end());
                wchar_t lChar;
                std::size_t lResult=mbrtowc(&lChar,lString.data(),lString.size(),&lState);
                if(lResult==(std::size_t)-1)
                {
                    msg_warn() << "illegal multibyte sequence\n";
                    ClearFIFO();
                }
                else if(lResult!=(std::size_t)-2)
                {
                    if(lResult!=mFIFO.size())
                    {
                        msg_warn() << "multibyte sequence didn't consume all characters";
                        for(auto i=mFIFO.begin();i!=mFIFO.end();++i)
                            msg_info() << (int)*i << " ";
                        msg_info() << "\n";
                    }
                    ClearFIFO();
                    return lChar;
                }
            }
        }
        else if(lKey)
        {
            ClearFIFO();
            return *lKey;
        }
        FillFIFO();
    }
}

void CScreen::Output(const std::string &pString)
{
    if(write(mOut,pString.data(),pString.length())<0)
    {
        throw XIO("can't output to terminal");
    }
}

void CScreen::Output(const std::wstring &pString)
{
    Output(utf8(pString));
}

void CScreen::SaveCursor()
{
    WriteSeq(save_cursor);
}

void CScreen::RestoreCursor()
{
    WriteSeq(restore_cursor);
}

void CScreen::CarriageReturn()
{
    WriteSeq(carriage_return);
}

void CScreen::CursorUp()
{
    WriteSeq(cursor_up);
}

void CScreen::CursorDown()
{
    WriteSeq(cursor_down);
}

void CScreen::CursorLeft()
{
    WriteSeq(cursor_left);
}

void CScreen::CursorRight()
{
    WriteSeq(cursor_right);
}

void CScreen::ClearToBOL()
{
    WriteSeq(clr_bol);
}

void CScreen::ClearToEOL()
{
    WriteSeq(clr_eol);
}

void CScreen::ClearToEOS()
{
    WriteSeq(clr_eos);
}

/*namespace tty*/ } /*namespace buola*/ }
